﻿
namespace Numbrella.Client
{
	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Reflection;
	using System.Security;
	using System.Security.Permissions;
	using System.Security.Policy;
	using System.ServiceModel;
	using System.ServiceModel.Channels;
	using Numbrella.Client.Common;
	using Numbrella.Interaction;

	internal sealed class TaskSolvingEngine
	{
		/// <summary>
		/// Contains application domain for sandboxing task.
		/// </summary>
		private AppDomain taskPerformSandbox;

		/// <summary>
		/// Contains proxy for calling <see cref="ITask.Perform"/> method in sandbox.
		/// </summary>
		private MarshalByRefTaskPerformProxy taskPerformProxy;

		private BackgroundWorker worker;

		private bool isSolvingTasksNow;

		private ProblemDefinition currentProblemDefinition;

		public TaskSolvingEngine()
		{
			this.CreateBackgroundWorker();
			this.CreateSandbox();
		}

		public event EventHandler ProblemDefinitionChanged;

		public event AsyncCompletedEventHandler SolveTasksCompleted;

		public ProblemDefinition CurrentProblemDefinition
		{
			get
			{
				return this.currentProblemDefinition;
			}

			private set
			{
				this.currentProblemDefinition = value;
				this.RaiseCurrentProblemDefinitionChanged(EventArgs.Empty);
			}
		}

		public void SolveTasksAsync()
		{
			Debug.Assert(!this.isSolvingTasksNow, "Can't start async task solving while previous thread still working.");
			this.isSolvingTasksNow = true;

			this.worker.RunWorkerAsync();
			Logger.Instance.AddRecord("Task solving engine started.");
		}

		public void CancelAsync()
		{
			Logger.Instance.AddRecord("Stopping task solving engine... Note that engine will actually stop only after solving current task.");
			this.worker.CancelAsync();
		}

		private static StrongName GetAssemblyStrongName(Assembly assembly)
		{
			Debug.Assert(assembly != null, "Assembly can not be null.");

			AssemblyName assemblyName = assembly.GetName();
			Debug.Assert(assemblyName != null, "Could not get assembly name.");

			byte[] publicKey = assemblyName.GetPublicKey();
			Debug.Assert(publicKey != null, "Public key can not be null.");

			StrongNamePublicKeyBlob keyBlob = new StrongNamePublicKeyBlob(publicKey);
			return new StrongName(keyBlob, assemblyName.Name, assemblyName.Version);
		}

		private void OnBackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			Logger.Instance.AddRecord("Task solving engine stopped.");
			
			this.isSolvingTasksNow = false;
			this.RaiseSolveTasksCompleted(new AsyncCompletedEventArgs(null, e.Cancelled, e.UserState));
		}

		private void OnBackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
		{
			while (!e.Cancel)
			{
				if (this.worker.CancellationPending)
				{
					e.Cancel = true;
					continue;
				}

				TrackerProxy trackerProxy = CommonFunctions.OpenConnectionToTracker(
					Settings.Current.TrackerConnectionString, Settings.Current.UserName, Settings.Current.Password);
				bool wasError = false;

				try
				{
					// Get description of pending problem
					this.CurrentProblemDefinition = trackerProxy.GetPendingProblem();
					trackerProxy.Close();
				}
				catch (TimeoutException exception)
				{
					Logger.Instance.AddRecord("Tracker network operation timed out. " + CommonFunctions.UnwrapExceptionMessage(exception));
					wasError = true;
				}
				catch (CommunicationException exception)
				{
					Logger.Instance.AddRecord("There was a communication problem with tracker. " + CommonFunctions.UnwrapExceptionMessage(exception));
					trackerProxy.Abort();
					wasError = true;
				}

				if (wasError)
				{
					trackerProxy.Abort();
					CommonFunctions.Timeout(Settings.Current.ErrorTimeout);
					continue;
				}

				if (this.CurrentProblemDefinition == null)
				{
					Logger.Instance.AddRecord("There were no pending problem.");
					CommonFunctions.Timeout(Settings.Current.NoPendingProblemTimeout);
					continue;
				}

				this.SolveCurrentProblem();
				this.CurrentProblemDefinition = null;
			}
		}

		private void CreateBackgroundWorker()
		{
			this.worker = new BackgroundWorker { WorkerSupportsCancellation = true };
			this.worker.DoWork += this.OnBackgroundWorkerDoWork;
			this.worker.RunWorkerCompleted += this.OnBackgroundWorkerRunWorkerCompleted;
		}

		private void CreateSandbox()
		{
			PermissionSet permissions = new PermissionSet(PermissionState.None);
			permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
			permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.ControlAppDomain));

			AppDomainSetup setup = new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory };
			this.taskPerformSandbox = AppDomain.CreateDomain(
				"Numbrella task execution jail",
				AppDomain.CurrentDomain.Evidence,
				setup,
				permissions,
				GetAssemblyStrongName(Assembly.GetExecutingAssembly()));

			Debug.Assert(Assembly.GetExecutingAssembly().FullName != null, "Assembly full name can not be null.");
			this.taskPerformProxy = (MarshalByRefTaskPerformProxy)
				this.taskPerformSandbox.CreateInstanceAndUnwrap(
					Assembly.GetExecutingAssembly().FullName,
					typeof(MarshalByRefTaskPerformProxy).FullName);
		}

		private void LoadAssemblyForProblem(ProblemDefinition definition)
		{
			// Load assembly
			byte[] rawAssembly = definition.Implementation.GetRawAssembly();
			Assembly assembly = Assembly.Load(rawAssembly);
			this.taskPerformProxy.LoadAssembly(rawAssembly);

			// Build known type list
			ResolveEventHandler handler = (sender, e) => e.Name == assembly.FullName ? assembly : null;
			AppDomain.CurrentDomain.AssemblyResolve += handler;
			List<Type> knownTypes = new List<Type>();
			foreach (string knownTypeName in definition.Implementation.SerializerKnownTypeNames)
				knownTypes.Add(Type.GetType(knownTypeName, true));
			AppDomain.CurrentDomain.AssemblyResolve -= handler;

			// Fill it into manager
			KnownTypeManager.FillTypes(knownTypes);
		}

		private void SolveCurrentProblem()
		{
			Debug.Assert(this.CurrentProblemDefinition != null, "Current problem definition can not be null when solving problem.");

			try
			{
				// Load assembly
				this.LoadAssemblyForProblem(this.CurrentProblemDefinition);

				// Create channel factory
				IChannelFactory<IProblem> problemFactory = new ChannelFactory<IProblem>(new NetTcpBinding(SecurityMode.None));

				// Retrive problem and task
				IProblem problem = problemFactory.CreateChannel(new EndpointAddress(this.CurrentProblemDefinition.Credentials.Address));
				ITask task = problem.GetPendingTask(Settings.Current.UserName);

				// Problem has no pending tasks
				if (task == null)
				{
					Logger.Instance.AddRecord("Redirected to problem '{0}' with no pending tasks.", this.CurrentProblemDefinition.Name);
					return;
				}

				Logger.Instance.AddRecord("Task {0} of problem '{1}' was taken for execution.", task.Id, this.CurrentProblemDefinition.Name);

				// Perform task in sandbox
				ITaskResult result = this.taskPerformProxy.PerformTask(task);

				// Submit result
				problem.SubmitResult(result, Settings.Current.UserName);

				Logger.Instance.AddRecord("Task {0} of problem '{1}' was solved.", task.Id, this.CurrentProblemDefinition.Name);
			}
			catch (TypeLoadException e)
			{
				Logger.Instance.AddRecord("Type load error while solving problem {0}. {1}", this.CurrentProblemDefinition.Name, CommonFunctions.UnwrapExceptionMessage(e));
			}
			catch (TimeoutException e)
			{
				Logger.Instance.AddRecord("Task owner network operation timed out. " + CommonFunctions.UnwrapExceptionMessage(e));
			}
			catch (CommunicationException e)
			{
				Logger.Instance.AddRecord("There was a communication problem. " + CommonFunctions.UnwrapExceptionMessage(e));
			}
			catch (SecurityException e)
			{
				Logger.Instance.AddRecord("There was security violation while solving problem. " + CommonFunctions.UnwrapExceptionMessage(e));
			}
			catch (FieldAccessException e)
			{
				Logger.Instance.AddRecord("There was reflection security violation while solving problem. " + CommonFunctions.UnwrapExceptionMessage(e));
			}
		}

		private void RaiseCurrentProblemDefinitionChanged(EventArgs e)
		{
			EventHandler handler = this.ProblemDefinitionChanged;
			if (handler != null)
				handler(this, e);
		}

		private void RaiseSolveTasksCompleted(AsyncCompletedEventArgs e)
		{
			AsyncCompletedEventHandler handler = this.SolveTasksCompleted;
			if (handler != null)
				handler(this, e);
		}
	}
}
